/*
 * AuthService.java
 *
 * Created on July 1, 2007, 1:22 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app.auth;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.UUID;
import java.util.logging.Logger;
import org.atomojo.app.admin.AdminXML;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.Item;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.XMLException;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.infoset.xml.util.WriterItemDestination;

/**
 *
 * @author alex
 */
public class FileAuthService implements AuthService
{
   static Logger LOG = Logger.getLogger(FileAuthService.class.getName());
   long lastModified;
   File authFile;
   Map<String,String> groups;
   Map<String,User> users;
   Map<String,String> passwords;
   DocumentLoader loader;
   public FileAuthService() {
      
   }
   public void init(Properties props)
      throws AuthException
   {
      loader = new SAXDocumentLoader();
      groups = new TreeMap<String,String>();
      users = new TreeMap<String,User>();
      passwords = new TreeMap<String,String>();
      URI baseURI = URI.create(props.getProperty("base-uri"));
      String href = props.getProperty("href");
      if (href==null) {
         throw new AuthException("Missing 'href' property.");
      }
      URI fileURI = baseURI.resolve(href);
      if (!fileURI.getScheme().equals("file")) {
         throw new AuthException("Unsupported scheme "+fileURI.getScheme());
      }
      authFile = new File(fileURI.getSchemeSpecificPart());
      if (!authFile.exists() && "true".equals(props.getProperty("create"))) {
         LOG.info("Creating auth file "+authFile);
         try {
            create();
         } catch (IOException ex) {
            throw new AuthException("I/O error on auth file creation for "+authFile,ex);
         } catch (XMLException ex) {
            throw new AuthException("XML error on auth file creation for "+authFile,ex);
         }
      }
      if (!authFile.exists()) {
         throw new AuthException("Auth file "+authFile+" does not exist.");
      }
      if (!authFile.canRead()) {
         throw new AuthException("Cannot read auth file "+authFile+" does not exist.");
      }
      try {
         load();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
   }

   private void create()
      throws AuthException,IOException,XMLException
   {
      createGroup(null,AuthService.ADMIN_GROUP);
      createUser(null,AuthService.ADMIN_USER,AuthService.ADMIN_USER,null,AuthService.ADMIN_USER);
      addUserToGroup(null,AuthService.ADMIN_USER,AuthService.ADMIN_GROUP);
      store();
   }
   
   private void load()
      throws IOException,XMLException
   {
      LOG.info("Loading auth from "+authFile);
      lastModified = authFile.lastModified();
      FileInputStream is = new FileInputStream(authFile);
      Reader reader = new InputStreamReader(is,"UTF-8");
      loader.generate(reader,authFile.toURI(),new ItemDestination() {
         public void send(Item item) 
            throws XMLException
         {
            if (item.getType()==Item.ItemType.ElementItem) {
               Element e = (Element)item;
               if (e.getName().equals(AdminXML.NM_USER)) {
                  String alias = e.getAttributeValue("alias");
                  if (alias==null) {
                     LOG.warning("Missing 'alias' attribute on user.");
                     return;
                  }
                  String uuid = e.getAttributeValue("id");
                  if (uuid==null) {
                     LOG.warning("Missing 'id' attribute on user.");
                     return;
                  }
                  String password = e.getAttributeValue("password");
                  if (password==null) {
                     password = e.getAttributeValue("md5-password");
                  } else {
                     try {
                        password = User.md5Password(password);
                     } catch (NoSuchAlgorithmException ex) {
                        throw new XMLException("Cannot MD5 password.",ex);
                     }
                  }
                  if (password==null) {
                     LOG.warning("Missing 'md5-password' or 'password' attribute on user.");
                     return;
                  }
                  String name = e.getAttributeValue("name");
                  if (name==null) {
                     name = alias;
                  }
                  String email = e.getAttributeValue("email");
                  LOG.fine(alias+","+uuid+","+name+","+email);
                  String groupsSpec = e.getAttributeValue("groups");
                  List<String> groupList = new ArrayList<String>();
                  if (groupsSpec!=null) {
                     String [] groupStrings = groupsSpec.split(",");
                     for (int i=0; i<groupStrings.length; i++) {
                        String group = groupStrings[i].trim();
                        if (group.length()!=0 && groups.get(group)!=null) {
                           LOG.fine(alias+": "+group);
                           groupList.add(group);
                        }
                     }
                  }
                  User u = new User(alias,UUID.fromString(uuid),name,email,groupList);
                  users.put(alias,u);
                  passwords.put(alias,password);
               } else if (e.getName().equals(AdminXML.NM_GROUP)) {
                  String name = e.getAttributeValue("name");
                  if (name!=null) {
                     groups.put(name,name);
                  }
               }
            }
         }
      });
      reader.close();
   }
   
   private void store()
      throws IOException,XMLException
   {
      File backupFile = new File(authFile.getAbsoluteFile().getParentFile(),authFile.getName()+".save");
      if (authFile.exists()) {
         if (!authFile.renameTo(backupFile)) {
            throw new IOException("Cannot backup file to "+backupFile);
         }
      }
      OutputStream os = new FileOutputStream(authFile);
      Writer w = new OutputStreamWriter(os,"UTF-8");

      generate(new WriterItemDestination(w,"UTF-8"));
      w.flush();
      w.close();
      lastModified = authFile.lastModified();
      if (backupFile.exists()) {
         backupFile.delete();
      }
   }
   
   private void generate(ItemDestination dest)
      throws XMLException
   {
      ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
      dest.send(constructor.createDocument());
      dest.send(constructor.createElement(AdminXML.NM_USERS));
      dest.send(constructor.createCharacters("\n"));
      for (String group : groups.values()) {
         Element groupE = constructor.createElement(AdminXML.NM_GROUP);
         groupE.setAttributeValue("name",group);
         dest.send(groupE);
         dest.send(constructor.createElementEnd(AdminXML.NM_GROUP));
         dest.send(constructor.createCharacters("\n"));
      }
      for (User user : users.values()) {
         Element userE = constructor.createElement(AdminXML.NM_USER);
         userE.setAttributeValue("alias",user.getAlias());
         userE.setAttributeValue("id",user.getId().toString());
         userE.setAttributeValue("md5-password",passwords.get(user.getAlias()));
         userE.setAttributeValue("name",user.getName());
         if (user.getEmail()!=null) {
            userE.setAttributeValue("email",user.getName());
         }
         String groups = "";
         for (String group : user.getGroups()) {
            if (groups.length()>0) {
               groups += ",";
               groups += group;
            } else {
               groups = group;
            }
         }
         userE.setAttributeValue("groups",groups);
         dest.send(userE);
         dest.send(constructor.createElementEnd(AdminXML.NM_USER));
         dest.send(constructor.createCharacters("\n"));
      }
      dest.send(constructor.createElementEnd(AdminXML.NM_USERS));
      dest.send(constructor.createDocumentEnd());
   }
   
   private void check() 
      throws IOException,XMLException
   {
      if (authFile.lastModified()>lastModified) {
         load();
      }
   }
   
   public boolean createGroup(AuthCredentials cred,String name)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      if (groups.get(name)!=null) {
         throw new AuthException("Group "+name+" already exists.");
      } 
      synchronized (groups) {
         groups.put(name,name);
         try {
            store();
         } catch (IOException ex) {
            throw new AuthException("I/O error on auth file "+authFile,ex);
         } catch (XMLException ex) {
            throw new AuthException("Cannot store auth file "+authFile,ex);
         }
      }
      return true;
   }
   
   public boolean deleteGroup(AuthCredentials cred,String name)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      synchronized (groups) {
         boolean retval = groups.remove(name)!=null;
         if (retval) {
            try {
               store();
            } catch (IOException ex) {
               throw new AuthException("I/O error on auth file "+authFile,ex);
            } catch (XMLException ex) {
               throw new AuthException("Cannot store auth file "+authFile,ex);
            }
         }
         return retval;
      }
   }
   
   public boolean addUserToGroup(AuthCredentials cred,String alias,String name)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      User user = users.get(alias);
      if (user==null) {
         throw new AuthException("User "+alias+" does not exist.");
      }
      List<String> newGroups = new ArrayList<String>();
      newGroups.addAll(user.getGroups());
      newGroups.add(name);
      User newUser = new User(user.getAlias(),user.getId(),user.getAlias(),user.getEmail(),newGroups);
      synchronized (users) {
         users.put(alias,newUser);
         try {
            store();
         } catch (IOException ex) {
            throw new AuthException("I/O error on auth file "+authFile,ex);
         } catch (XMLException ex) {
            throw new AuthException("Cannot store auth file "+authFile,ex);
         }
      }
      return true;
   }
   public boolean removeUserFromGroup(AuthCredentials cred,String alias,String name)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      User user = users.get(alias);
      if (user==null) {
         return false;
      }
      List<String> newGroups = new ArrayList<String>();
      newGroups.addAll(user.getGroups());
      newGroups.remove(name);
      User newUser = new User(user.getAlias(),user.getId(),user.getName(),user.getEmail(),newGroups);
      synchronized (users) {
         users.put(alias,newUser);
         try {
            store();
         } catch (IOException ex) {
            throw new AuthException("I/O error on auth file "+authFile,ex);
         } catch (XMLException ex) {
            throw new AuthException("Cannot store auth file "+authFile,ex);
         }
      }
      return true;
   }
   
   public User createUser(AuthCredentials cred,String alias,String name,String email,String password) 
      throws AuthException
   {
      UUID uid = UUID.randomUUID();
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      User user = users.get(alias);
      if (user!=null) {
         throw new AuthException("User "+alias+" does not exist.");
      }
      User newUser = new User(alias,uid,name,email,new ArrayList<String>());
      synchronized (users) {
         users.put(alias,newUser);
         try {
            passwords.put(alias,User.md5Password(password));
         } catch (NoSuchAlgorithmException ex) {
            throw new AuthException("Cannot MD5 password.",ex);
         }
         try {
            store();
         } catch (IOException ex) {
            throw new AuthException("I/O error on auth file "+authFile,ex);
         } catch (XMLException ex) {
            throw new AuthException("Cannot store auth file "+authFile,ex);
         }
      }
      return newUser;
   }
   
   public boolean deleteUser(AuthCredentials cred,String alias)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      synchronized (users) {
         boolean retval = users.remove(alias)!=null;
         if (retval) {
            try {
               store();
            } catch (IOException ex) {
               throw new AuthException("I/O error on auth file "+authFile,ex);
            } catch (XMLException ex) {
               throw new AuthException("Cannot store auth file "+authFile,ex);
            }
         }
         return retval;
      }
   }
   
   public User getUser(AuthCredentials cred,String alias)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      return users.get(alias);
   }
   
   public Iterator<User> getUsers(AuthCredentials cred)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      return users.values().iterator();
   }
   
   
   public boolean updateUser(AuthCredentials cred,String alias,String name,String email)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      User user = users.get(alias);
      if (user==null) {
         throw new AuthException("User "+alias+" does not exist.");
      }
      User newUser = new User(user.getAlias(),user.getId(),name,email,user.getGroups());
      synchronized (users) {
         users.put(alias,newUser);
         try {
            store();
         } catch (IOException ex) {
            throw new AuthException("I/O error on auth file "+authFile,ex);
         } catch (XMLException ex) {
            throw new AuthException("Cannot store auth file "+authFile,ex);
         }
      }
      return true;
   }
   
   public boolean setPassword(AuthCredentials cred,String alias,String password)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      synchronized (users) {
         try {
            passwords.put(alias,User.md5Password(password));
            try {
               store();
            } catch (IOException ex) {
               throw new AuthException("I/O error on auth file "+authFile,ex);
            } catch (XMLException ex) {
               throw new AuthException("Cannot store auth file "+authFile,ex);
            }
         } catch (NoSuchAlgorithmException ex) {
            throw new AuthException("Unable to MD5 password.",ex);
         }
      }
      return true;
   }
   
   public User authenticate(String alias,String password)
      throws AuthException
   {
      try {
         check();
      } catch (IOException ex) {
         throw new AuthException("I/O error on auth file "+authFile,ex);
      } catch (XMLException ex) {
         throw new AuthException("Cannot parse auth file "+authFile,ex);
      }
      try {
         String md5 = User.md5Password(password);
         String checkMd5 = passwords.get(alias);
         if (md5.equals(checkMd5)) {
            return users.get(alias);
         } else {
            return null;
         }
      } catch (NoSuchAlgorithmException ex) {
         throw new AuthException("Unable to MD5 password.",ex);
      }
   }
   
   public User verifySession(String session)
      throws AuthException
   {
      return null;
   }

}
